본문으로 건너뛰기

Slack → GitHub 역방향 봇 구현 계획

Goal: 슬랙 멘션으로 rogue-docs 레포에 문서 커밋/수정/미팅노트 생성하는 봇 구현

Architecture: Next.js API Route로 Slack Event API 수신 → Claude로 의도 파악 → GitHub API로 레포 조작 → 슬랙에 결과 회신

Tech Stack: Next.js (App Router), Anthropic SDK, Slack Bolt, Octokit (GitHub API), Vitest

코드 위치: 4-프로젝트/기본 업무 환경 구축/slack-bot/


Task 1: 프로젝트 스캐폴딩

Files:

  • Create: slack-bot/package.json
  • Create: slack-bot/next.config.ts
  • Create: slack-bot/tsconfig.json
  • Create: slack-bot/.env.example
  • Create: slack-bot/.gitignore

Step 1: Next.js 프로젝트 초기화

cd "4-프로젝트/기본 업무 환경 구축/slack-bot"
npx create-next-app@latest . --ts --app --tailwind=no --eslint=no --src-dir=no --import-alias="@/*"

Step 2: 의존성 설치

npm install @slack/bolt @anthropic-ai/sdk octokit
npm install -D vitest @types/node

Step 3: 환경변수 템플릿 생성

.env.example:

SLACK_BOT_TOKEN=xoxb-...
SLACK_SIGNING_SECRET=...
ANTHROPIC_API_KEY=sk-ant-...
GITHUB_PAT=ghp_...
GITHUB_REPO_OWNER=...
GITHUB_REPO_NAME=rogue-docs

Step 4: 커밋

git add "4-프로젝트/기본 업무 환경 구축/slack-bot/"
git commit -m "feat: slack-bot 프로젝트 스캐폴딩"

Task 2: Slack 서명 검증 유틸

Files:

  • Create: slack-bot/lib/verify-slack.ts
  • Create: slack-bot/lib/verify-slack.test.ts

Step 1: 테스트 작성

// verify-slack.test.ts
import { describe, it, expect } from 'vitest';
import { verifySlackRequest } from './verify-slack';

describe('verifySlackRequest', () => {
it('유효한 서명이면 true 반환', () => {
// 테스트용 시크릿과 타임스탬프로 HMAC 생성
const secret = 'test-signing-secret';
const timestamp = '1609459200';
const body = '{"type":"url_verification"}';
// 실제 HMAC 계산해서 검증
const result = verifySlackRequest({ secret, timestamp, body, signature: 'computed' });
expect(result).toBeDefined();
});

it('잘못된 서명이면 false 반환', () => {
const result = verifySlackRequest({
secret: 'test',
timestamp: '1609459200',
body: '{}',
signature: 'v0=invalid'
});
expect(result).toBe(false);
});
});

Step 2: 테스트 실패 확인

npx vitest run lib/verify-slack.test.ts

Expected: FAIL — verifySlackRequest is not defined

Step 3: 구현

// verify-slack.ts
import crypto from 'crypto';

export function verifySlackRequest({ secret, timestamp, body, signature }: {
secret: string;
timestamp: string;
body: string;
signature: string;
}): boolean {
const sigBasestring = `v0:${timestamp}:${body}`;
const mySignature = 'v0=' + crypto
.createHmac('sha256', secret)
.update(sigBasestring)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(mySignature),
Buffer.from(signature)
);
}

Step 4: 테스트 통과 확인 → 커밋


Task 3: Slack 이벤트 수신 API Route

Files:

  • Create: slack-bot/app/api/slack/events/route.ts

Step 1: URL Verification + app_mention 핸들러 뼈대

Slack은 Event API 등록 시 url_verification challenge를 보냄. 이걸 먼저 처리하고, app_mention 이벤트를 받아서 처리하는 구조.

// route.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifySlackRequest } from '@/lib/verify-slack';

export async function POST(req: NextRequest) {
const body = await req.text();
const timestamp = req.headers.get('x-slack-request-timestamp') ?? '';
const signature = req.headers.get('x-slack-signature') ?? '';

// 서명 검증
if (!verifySlackRequest({
secret: process.env.SLACK_SIGNING_SECRET!,
timestamp, body, signature
})) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}

const payload = JSON.parse(body);

// URL Verification (Slack 앱 등록 시 1회)
if (payload.type === 'url_verification') {
return NextResponse.json({ challenge: payload.challenge });
}

// app_mention 이벤트
if (payload.event?.type === 'app_mention') {
// 3초 내 응답 필요 → 비동기 처리
waitUntil(handleMention(payload.event));
return NextResponse.json({ ok: true });
}

return NextResponse.json({ ok: true });
}

Step 2: 커밋


Task 4: Slack 스레드 컨텍스트 수집

Files:

  • Create: slack-bot/lib/slack-client.ts
  • Create: slack-bot/lib/slack-client.test.ts

Step 1: 테스트 작성

describe('fetchThreadMessages', () => {
it('스레드 메시지를 시간순으로 반환', async () => {
// Slack API mock
const messages = await fetchThreadMessages({
channel: 'C123',
threadTs: '1609459200.000100',
token: 'xoxb-test'
});
expect(Array.isArray(messages)).toBe(true);
});
});

Step 2: 구현

Slack conversations.replies API로 스레드 전체 메시지 수집.

export async function fetchThreadMessages({ channel, threadTs, token }: {
channel: string;
threadTs: string;
token: string;
}) {
const res = await fetch(`https://slack.com/api/conversations.replies?channel=${channel}&ts=${threadTs}`, {
headers: { Authorization: `Bearer ${token}` }
});
const data = await res.json();
return data.messages ?? [];
}

Step 3: 테스트 통과 → 커밋


Task 5: AI 의도 파악 + 액션 라우팅

Files:

  • Create: slack-bot/lib/ai-router.ts
  • Create: slack-bot/lib/ai-router.test.ts

Step 1: 테스트 작성

describe('parseIntent', () => {
it('문서 커밋 요청을 인식', async () => {
const intent = await parseIntent('이 스레드 내용 정리해서 깃헙에 올려줘');
expect(intent.action).toBe('commit-doc');
});

it('미팅노트 생성 요청을 인식', async () => {
const intent = await parseIntent('이 논의 미팅노트로 만들어줘');
expect(intent.action).toBe('create-meeting-note');
});
});

Step 2: 구현

Claude에게 시스템 프롬프트로 액션 분류 + 파라미터 추출 요청.

import Anthropic from '@anthropic-ai/sdk';

type Intent = {
action: 'commit-doc' | 'update-doc' | 'create-meeting-note' | 'unknown';
targetProject?: string;
fileName?: string;
summary?: string;
};

export async function parseIntent(userMessage: string, threadContext?: string): Promise<Intent> {
const client = new Anthropic();
const response = await client.messages.create({
model: 'claude-sonnet-4-6-20250320',
max_tokens: 500,
system: `너는 Slack 봇이다. 사용자의 요청을 분석해서 JSON으로 반환해라.
가능한 action: commit-doc, update-doc, create-meeting-note, unknown
rogue-docs 프로젝트 폴더: 병원 PoC, 법률 PoC, 크리에이터 PoC, 마오타이 마케팅 제안, 기본 업무 환경 구축`,
messages: [{ role: 'user', content: userMessage }]
});
// JSON 파싱
return JSON.parse(response.content[0].text);
}

Step 3: 테스트 통과 → 커밋


Task 6: GitHub 커밋 액션

Files:

  • Create: slack-bot/lib/github-client.ts
  • Create: slack-bot/lib/github-client.test.ts

Step 1: 테스트 작성

describe('commitFile', () => {
it('새 파일 커밋 성공 시 커밋 URL 반환', async () => {
const result = await commitFile({
path: '3-미팅/2026-03-23-테스트.md',
content: '# 테스트',
message: 'feat: 미팅노트 자동 생성'
});
expect(result.url).toContain('github.com');
});
});

Step 2: 구현

Octokit으로 GitHub Contents API 호출.

import { Octokit } from 'octokit';

export async function commitFile({ path, content, message }: {
path: string;
content: string;
message: string;
}) {
const octokit = new Octokit({ auth: process.env.GITHUB_PAT });
const res = await octokit.rest.repos.createOrUpdateFileContents({
owner: process.env.GITHUB_REPO_OWNER!,
repo: process.env.GITHUB_REPO_NAME!,
path,
message,
content: Buffer.from(content).toString('base64'),
});
return { url: res.data.content?.html_url ?? '' };
}

Step 3: 테스트 통과 → 커밋


Task 7: 문서 생성 액션 (Claude로 정리)

Files:

  • Create: slack-bot/lib/actions/commit-doc.ts
  • Create: slack-bot/lib/actions/create-meeting-note.ts

Step 1: 구현

스레드 메시지 → Claude로 정리 → GitHub 커밋. 각 액션별 프롬프트와 파일 경로 규칙 분리.

// commit-doc.ts
export async function commitDoc({ messages, project }: { messages: string[]; project: string }) {
// 1. Claude로 논의 내용 정리
// 2. 파일명 생성 (날짜-프로젝트-주제.md)
// 3. GitHub에 커밋
// 4. 결과 반환
}

// create-meeting-note.ts
export async function createMeetingNote({ messages }: { messages: string[] }) {
// 1. Claude로 미팅노트 형식으로 정리
// 2. 3-미팅/날짜-프로젝트-주제.md 경로로 커밋
// 3. 결과 반환
}

Step 2: 커밋


Task 8: 메인 핸들러 조립

Files:

  • Create: slack-bot/lib/handle-mention.ts

Step 1: 구현

모든 모듈을 조합하는 메인 흐름.

export async function handleMention(event: SlackEvent) {
// 1. 스레드 컨텍스트 수집
// 2. AI로 의도 파악
// 3. 액션 실행 (commit-doc / update-doc / create-meeting-note)
// 4. Slack에 결과 회신 (chat.postMessage)
}

Step 2: API Route에서 이 함수 호출하도록 연결

Step 3: 커밋


Task 9: Slack 앱 생성 가이드 + Vercel 배포

Step 1: Slack 앱 생성 (수동)

  • https://api.slack.com/apps 에서 앱 생성
  • Event Subscriptions 활성화, Request URL 설정
  • app_mention 이벤트 구독
  • Bot Token Scopes: app_mentions:read, chat:write, channels:history

Step 2: Vercel 프로젝트 연결

cd "4-프로젝트/기본 업무 환경 구축/slack-bot"
vercel link
vercel env add SLACK_BOT_TOKEN
vercel env add SLACK_SIGNING_SECRET
vercel env add ANTHROPIC_API_KEY
vercel env add GITHUB_PAT

Step 3: 배포 + Slack Request URL 업데이트

vercel --prod
# Slack 앱 설정에서 Request URL을 배포 URL로 변경

Step 4: 슬랙에서 봇 멘션 테스트 → 커밋